<?php
/**
* 名称：文件上传类。
* 描述：将"上传文件"所需的功能包装在一起，组织起来提供完善的PHP上传功能。
*/
final class Uploader{
    /**
    * 描述：自定义错误。
    */
    const UPLOAD_ERR=-999;                             //PHP系统中除上传成功的其它错误总代称。@
    const UPLOAD_ERR_CUSTOM_ATTRIBUTE=-8;              //属性值错误。
    const UPLOAD_ERR_CUSTOM_MOVE=-7;                   //文件移动失败。
    const UPLOAD_ERR_CUSTOM_FILENAME=-6;               //非法文件名。
    const UPLOAD_ERR_CUSTOM_AUTONAMEFUNCTION=-5;       //自动生成文件名函数错误。
    const UPLOAD_ERR_CUSTOM_PATH=-4;                   //上传路径不存在。
    const UPLOAD_ERR_CUSTOM_EXT=-3;                    //扩展名不符合设置要求。@
    const UPLOAD_ERR_CUSTOM_SIZE=-2;                   //文件大小不符合设置要求。@
    const UPLOAD_ERR_CUSTOM_FILEFIELD=-1;              //文件域不存在。
    const UPLOAD_ERR_OK=UPLOAD_ERR_OK;                 //文件上传成功。@
    const UPLOAD_ERR_INI_SIZE=UPLOAD_ERR_INI_SIZE;     //上传文件大小超出了php.ini中upload_max_filesize的值。
    const UPLOAD_ERR_FORM_SIZE=UPLOAD_ERR_FORM_SIZE;   //上传文件大小超出了表单中MAX_FILE_SIZE的值。
    const UPLOAD_ERR_PARTIAL=UPLOAD_ERR_PARTIAL;       //文件只上传了一部分，没有被全部上传。
    const UPLOAD_ERR_NO_FILE=UPLOAD_ERR_NO_FILE;       //文件域中没有文件。
    const UPLOAD_ERR_NO_TMP_DIR=UPLOAD_ERR_NO_TMP_DIR; //找不到临时目录。
    const UPLOAD_ERR_CANT_WRITE=UPLOAD_ERR_CANT_WRITE; //写入失败，可能没有权限。
    const UPLOAD_ERR_EXTENSION=UPLOAD_ERR_EXTENSION;   //文件上传过程中被停止。

    /**
    * 描述：支持数据格式常值。
    */
    const UPLOAD_EXT_IMAGE='gif，jpg，png，bmp';            //图像文件。
    const UPLOAD_EXT_FILE='pdf，txt，doc，docx，xls，xlsx'; //文档文件。

    /**
    * 描述：上传配置。
    */
    public $Size=204800;             //指定上传文件要符合的大小。
    public $FileExtension=NULL;       //指定上传文件要我们要符合的扩展名。
    public $Path=NULL;               //指定上传文件要上传的路径。
    public $AutoNaming=TRUE;         //自动命名。
    public $AutoNamingFunction=NULL; //自动命名函数。
    public $AutoPath=TRUE;           //自动路径。
    public $MultiType='all';         //all表示全部正常才上传并默认，part表示只上传正常的部分。

    /**
    * 描述：过程标量。
    */
    private $UploadType='single';        //single是单文件上传并默认，multi是多文件上传。
    private $UploadFileObject=array();   //文件上传对象。
    private $UploadFileObjectArray=NULL; //文件上传对象组。
    private $ErrorCode=NULL;             //存储上传过程中错误代码。
    private $ErrorMessage=NULL;          //存储上传过程中错误信息。
    private $Filename=NULL;              //存储上传后的文件名。

    /**
    * 描述：上传过程辅助标量。
    */
    private $PHPUploadErrorName=array(
        UPLOAD_ERR_OK=>'UPLOAD_ERR_OK',                 //文件上传成功。@
        UPLOAD_ERR_INI_SIZE=>'UPLOAD_ERR_INI_SIZE',     //上传文件大小超出了php.ini中upload_max_filesize的值。
        UPLOAD_ERR_FORM_SIZE=>'UPLOAD_ERR_FORM_SIZE',   //上传文件大小超出了表单中MAX_FILE_SIZE的值。
        UPLOAD_ERR_PARTIAL=>'UPLOAD_ERR_PARTIAL',       //文件只上传了一部分，没有被全部上传。
        UPLOAD_ERR_NO_FILE=>'UPLOAD_ERR_NO_FILE',       //文件域中没有文件。
        UPLOAD_ERR_NO_TMP_DIR=>'UPLOAD_ERR_NO_TMP_DIR', //找不到临时目录。
        UPLOAD_ERR_CANT_WRITE=>'UPLOAD_ERR_CANT_WRITE', //写入失败，可能没有权限。
        UPLOAD_ERR_EXTENSION=>'UPLOAD_ERR_EXTENSION'    //文件上传过程中被停止。
    );

    /**
    * 描述：构造函数。
    * @param  uploadfileObject $fileObject 可选 文件上传对象。
    * @return void
    */
    public function __Construct(){
        $argumentCount=func_num_args();
        if($argumentCount>0){
            $arguments=func_get_args();
            if(1===$argumentCount){
                if(isset($_FILES[$arguments[0]])){
                    $this->UploadFileObject=$_FILES[$arguments[0]];
                }
                else{
                    $this->SetError(self::UPLOAD_ERR_CUSTOM_FILEFIELD,$this->MakeErrorMessage(
                        '__Construct',
                        'UPLOAD_ERR_CUSTOM_FILEFIELD',
                        self::UPLOAD_ERR_CUSTOM_FILEFIELD,
                        sprintf('File field not exist,the name is %s.',$arguments[0])
                    ));
                }
            }
            else{
                $this->UploadType='multi'; //设置为多文件上传。
                for($i=0;$i<$argumentCount;$i++){
                    if(isset($_FILES[$arguments[$i]])){
                        $this->UploadFileObject[$arguments[$i]]=$_FILES[$arguments[$i]];
                    }
                    else{
                        $this->SetError(self::UPLOAD_ERR_CUSTOM_FILEFIELD,$this->MakeErrorMessage(
                            '__Construct',
                            'UPLOAD_ERR_CUSTOM_FILEFIELD',
                            self::UPLOAD_ERR_CUSTOM_FILEFIELD,
                            sprintf('File field not exist,the name is %s.',$arguments[$i])
                        ));
                        break;
                    }
                }
            }
        }
    }

    /**
    * 描述：获取相关属性。
    * @param  string $name 必选 要获取的属性名。
    * @return mix
    */
    public function __Get($name){
        switch ($name){
            case 'ErrorCode':
                return $this->ErrorCode;
            break;
            case 'ErrorMessage':
                return $this->ErrorMessage;
            break;
            case 'Filename':
                return $this->Filename;
            break;
        }
    }

    /**
    * 描述：增加文件上传对象，针对多文件上传。
    */
    public function AddFile($fileObject,$filesize,$expendName,$uploadPath){

    }

    /**
    * 描述：根据文件调用方式执行上传文件过程。
    * @return void
    */
    public function Execute(){
        #初始化时有错，直接返回。
        if(!empty($this->ErrorCode)||$this->CheckAttribute()!==TRUE){
            return FALSE;
        }
        #单文件上传与多文件上传分别处理。
        if('multi'===$this->UploadType){
            foreach($this->UploadFileObject as &$uploadFile){
                if(isset($uploadFile['upload'])){
                    if(empty($uploadFile['upload']['size'])){
                        $uploadFile['upload']['size']=$this->Size;
                    }
                    if(empty($uploadFile['upload']['fileExtension'])){
                        $uploadFile['upload']['fileExtension']=$this->FileExtension;
                    }
                    if(empty($uploadFile['upload']['path'])){
                        $uploadFile['upload']['path']=$this->Path;
                    }
                }
                else{
                    $uploadFile['upload']=array(
                        'size'=>$this->Size,
                        'fileExtension'=>$this->FileExtension,
                        'path'=>$this->Path
                    );
                }
                //如果要求全部合格再上传则检查。
                if('all'===$this->MultiType&&!$this->CheckUploadFileStatus()){
                    return FALSE;
                }
                //上传文件。
                $result=$this->Upload($uploadFile,$uploadFile['upload']['size'],$uploadFile['upload']['fileExtension'],$uploadFile['upload']['path']);
                if($result['IsUpload']){
                    $this->UploadReport['SuccessfulCount']+=1;                                       //上传成功数+1。
                    $this->UploadReport['SuccessfulField'][]=$result['FieldName'];                   //统计上传成功的文件域名称。
                    $this->UploadReport['SuccessfulOriginalFilename'][]=$result['OriginalFilename']; //统计上传成功的原文件名称。
                    $this->UploadReport['SuccessfulNewFilename'][]=$result['NewFilename'];           //统计上传成功的新文件名称。
                }
                else{
                    $this->UploadReport['UnsuccessfulCount']+=1;                                       //上传失败数+1。
                    $this->UploadReport['UnsuccessfulField'][]=$result['FieldName'];                   //统计上传失败的文件域名称。
                    $this->UploadReport['UnsuccessfulOriginalFilename'][]=$result['OriginalFilename']; //统计上传失败的原文件名称。
                    $this->UploadReport['UnsuccessfulNewFilename'][]=$result['NewFilename'];           //统计上传失败的新文件名称。
                }
            }
        }
        else{
            $this->Upload($this->UploadFileObject,$this->Size,$this->FileExtension,$this->Path); //单文件上传。
        }
    }

    /**
    * 描述：设置上传属性。
    * @param  string $name  必选 要设置的上传属性名。
    * @param  mix    $value 必选 要设置的上传属性值。
    * @return void
    */
    private function CheckAttribute(){
        #Size
        if(!is_numeric($this->Size)){
            $this->SetError(self::UPLOAD_ERR_ATTRIBUTE,$this->MakeErrorMessage(
                'CheckAttribute::Size',
                'UPLOAD_ERR_ATTRIBUTE',
                self::UPLOAD_ERR_ATTRIBUTE,
                'Size value is not numeric.'
            ));
            return FALSE;
        }
        #FileExtension
        if(!is_array($this->FileExtension)||array_keys($this->FileExtension)!==range(0,count($this->FileExtension)-1)){ //如果数字数组。
            $this->SetError(self::UPLOAD_ERR_ATTRIBUTE,$this->MakeErrorMessage(
                'CheckAttribute::FileExtension',
                'UPLOAD_ERR_ATTRIBUTE',
                self::UPLOAD_ERR_ATTRIBUTE,
                "FileExtension value type must be digital array,for example:array('gif','png')."
            ));
            return FALSE;
        }
        #Path
        if(!isset($this->Path)&&empty($this->Path)){
            $this->SetError(self::UPLOAD_ERR_ATTRIBUTE,$this->MakeErrorMessage(
                'CheckAttribute::Path',
                'UPLOAD_ERR_ATTRIBUTE',
                self::UPLOAD_ERR_ATTRIBUTE,
                'Upload path value is NULL or empty.'
            ));
            return FALSE;
        }
        #AutoNamingFunction
        if(isset($this->AutoNamingFunction)){
            if('object'!==gettype($this->AutoNamingFunction)){
                $this->SetError(self::UPLOAD_ERR_ATTRIBUTE,$this->MakeErrorMessage(
                    'CheckAttribute::AutoNamingFunction',
                    'UPLOAD_ERR_ATTRIBUTE',
                    self::UPLOAD_ERR_ATTRIBUTE,
                    'AutoNamingFunction value is not Funtion.'
                ));
                return FALSE;
            }
        }
        #AutoPath
        if(!is_dir($this->Path)){
            if($this->AutoPath){
                $this->CreatePath($this->Path);
            }
            else{
                $this->SetError(self::UPLOAD_ERR_ATTRIBUTE,$this->MakeErrorMessage(
                    'CheckAttribute::AutoPath',
                    'UPLOAD_ERR_ATTRIBUTE',
                    self::UPLOAD_ERR_ATTRIBUTE,
                    sprintf('Upload path not exist,Meanwhile autoPath attribute value is FALSE,the path is %s.',$this->Path)
                ));
                return FALSE;
            }
        }
        return TRUE;
    }

    /**
    * 描述：上传文件。
    * @return FALSE  文上传失败返回FALSE。
    * @return string 成功返回文件名。
    * @return array  出错返回出错对象。
    */
    private function Upload($fileObject,$filesize=NULL,$fileExtension=NULL,$uploadPath=NULL){
        if(self::UPLOAD_ERR_OK===$fileObject['error']){
            #文件大小不合适。
            if($fileObject['size']>$filesize){
                $this->SetError(self::UPLOAD_ERR_CUSTOM_SIZE,$this->MakeErrorMessage(
                    'Upload',
                    'UPLOAD_ERR_CUSTOM_SIZE',
                    self::UPLOAD_ERR_CUSTOM_SIZE,
                    sprintf('Filesize greater than option size. option size is %s.',$this->Size)
                ));
                return;
            }
            #扩展名不合适。
            $fileExtensionPragmatic=$this->GetExtName($fileObject['name']);
            if($fileExtension){
                if(!in_array($fileExtensionPragmatic,$fileExtension)){
                    $this->SetError(self::UPLOAD_ERR_EXTENSION,$this->MakeErrorMessage(
                        'Upload',
                        'UPLOAD_ERR_EXTENSION',
                        self::UPLOAD_ERR_EXTENSION,
                        sprintf('File extension is error,option extension is %s,upload file extension is %s.',implode(' or ',$this->FileExtension),$fileExtensionPragmatic)
                    ));
                    return;
                }
            }
            #生成文件名
            $filename=$fileObject['name'];
            if($this->AutoNaming){
                if($this->AutoNamingFunction){
                    $func=$this->AutoNamingFunction;
                    $filename=$func($filename);
                }
                else{
                    $filename=date('Ymd').time().rand(1000,2000);
                }
                //检测文件名
                $regexpMatchResult=array();
                $regexpCountResult=preg_match_all('/[a-zA-Z0-9_-]+/',$filename,$regexpMatchResult);
                if(1!==$regexpCountResult||FALSE===$regexpCountResult){
                    $this->SetError(self::UPLOAD_ERR_CUSTOM_FILENAME,$this->MakeErrorMessage(
                        'Upload',
                        'UPLOAD_ERR_CUSTOM_FILENAME',
                        self::UPLOAD_ERR_CUSTOM_FILENAME,
                        sprintf('Create the file name is nonlicet,filename regular expression is `%s`,filename is %s.','/[a-zA-Z0-9_-]+/',$filename)
                    ));
                    return;
                }
                $filename=$filename.'.'.$fileExtensionPragmatic;
            }
            #上传文件
            if(@move_uploaded_file($fileObject['tmp_name'],str_replace('//','/',$uploadPath).'/'.$filename)){
                $this->Filename=$filename;
            }
            else{
                $this->SetError(self::UPLOAD_ERR_CUSTOM_MOVE,$this->MakeErrorMessage(
                    'Upload',
                    'UPLOAD_ERR_CUSTOM_MOVE',
                    self::UPLOAD_ERR_CUSTOM_MOVE,
                    'Move file failed.'
                ));
            }
        }
        else{
            $this->SetError($fileObject['error'],$this->MakeErrorMessage(
                'Upload',
                 $this->PHPUploadErrorName[$fileObject['error']],
                 $fileObject['error'],
                 'PHP engine upload error.'
            ));
        }
    }

    /**
    * 描述：设置错误，并按错误类型写错误日志。
    */
    private function SetError($errorCode,$errorMessge){
        $this->ErrorCode=$errorCode;
        $this->ErrorMessage=$errorMessge;
        error_log($errorMessge);
    }

    /**
    * 描述：设置错误，并按错误类型写错误日志。
    */
    private function MakeErrorMessage($errorSource,$errorLiterals,$errorCode,$errorMessage){
        return sprintf('TryLibaray::Uploader::%s,literals is `%s`,error code is `%s`,error message is `%s`',
            $errorSource,$errorLiterals,$errorCode,$errorMessage
        );
    }

    /**
    * 描述：获取扩展名。
    * @param  file   require $file 文件对象
    * @return string
    */
    private function GetExtName($filename){
        $tempArray=explode('.', $filename);
        $value=strtolower($tempArray[count($tempArray)-1]);
        return $value;
    }

    /**
    * 描述：自动创建文件上传路径。
    * @param  string $path 必选 路径。
    * @return void
    */
    private function CreatePath($path){
        if(!is_dir($path)){
            $path=explode('/',str_replace('\\','/',$path));
            for($i=0;$i<count($path);$i++){
                $temp=join('/',array_slice($path,0,$i+1));
                if(!is_dir($temp)){
                    mkdir($temp);
                }
            }
        }
    }

    /**
    * 描述：从人理解的角度计算文件大小，并返回相应值。
    * @param  file   require $file 文件对象
    * @return string
    */
    public static function SizeFormat($bytesize){
        $i=0;
        #当$bytesize 大于是1024字节时，开始循环，当循环到第4次时跳出；
        while(abs($bytesize)>=1024){
            $bytesize=$bytesize/1024;
            $i++;
            if($i==4){
                break;
            }
        }
        #将Bytes,KB,MB,GB,TB定义成一维数组；
        $units= array("bytes","kb","mb","gb","tb");
        $newsize=round($bytesize,2);
        return("$newsize $units[$i]");
    }
}
?>